/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.code.motoo.management;

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.beans.PropertyEditor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ThreadPoolExecutor;

import javax.management.Attribute;
import javax.management.AttributeChangeNotification;
import javax.management.AttributeList;
import javax.management.AttributeNotFoundException;
import javax.management.InstanceNotFoundException;
import javax.management.InvalidAttributeValueException;
import javax.management.ListenerNotFoundException;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanNotificationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.MBeanRegistration;
import javax.management.MBeanServer;
import javax.management.Notification;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.RuntimeOperationsException;
import javax.management.modelmbean.InvalidTargetObjectTypeException;
import javax.management.modelmbean.ModelMBean;
import javax.management.modelmbean.ModelMBeanAttributeInfo;
import javax.management.modelmbean.ModelMBeanConstructorInfo;
import javax.management.modelmbean.ModelMBeanInfo;
import javax.management.modelmbean.ModelMBeanInfoSupport;
import javax.management.modelmbean.ModelMBeanNotificationInfo;
import javax.management.modelmbean.ModelMBeanOperationInfo;

/**
 * 
 * @author linfengqi
 * @version 1.0  May 13, 2009
 */
public class BaseMBean<T> implements ModelMBean, MBeanRegistration {
	
	private T source;
	private MBeanInfo info;
	private volatile MBeanServer server;
    private volatile ObjectName name;
    private final Map<String, PropertyDescriptor> propertyDescriptors = new HashMap<String, PropertyDescriptor>();

	public BaseMBean(T source) {
		this.source = source;
		this.info = createModelMBeanInfo(source);
	}
	
	private MBeanInfo createModelMBeanInfo(T source) {
		String className = source.getClass().getName();
		String description = "";

		ModelMBeanConstructorInfo[] constructors = new ModelMBeanConstructorInfo[0];
		ModelMBeanNotificationInfo[] notifications = new ModelMBeanNotificationInfo[0];

		List<ModelMBeanAttributeInfo> attributes = new ArrayList<ModelMBeanAttributeInfo>();
		List<ModelMBeanOperationInfo> operations = new ArrayList<ModelMBeanOperationInfo>();

		addAttributes(attributes, source);
		addOperations(operations, source);
		operations.add(new ModelMBeanOperationInfo("unregisterMBean",
				"unregisterMBean", 
				new MBeanParameterInfo[0], 
				void.class.getName(), 
				ModelMBeanOperationInfo.ACTION));

		return new ModelMBeanInfoSupport(className, description, 
				attributes.toArray(new ModelMBeanAttributeInfo[attributes.size()]),
				constructors,
				operations.toArray(new ModelMBeanOperationInfo[operations.size()]), 
				notifications);
	}
	
	private void addAttributes(List<ModelMBeanAttributeInfo> attributes, T source) {
		addAttributes(attributes, source, source.getClass(), "");
	}

	private void addAttributes(List<ModelMBeanAttributeInfo> attributes, Object object, Class<?> type, String prefix) {
		PropertyDescriptor[] pdescs;
		try {
			pdescs = Introspector.getBeanInfo(type).getPropertyDescriptors();
		} catch (IntrospectionException e) {
			return;
		}
		
		for (PropertyDescriptor pdesc : pdescs) {
			// Ignore a write-only property.
			if (pdesc.getReadMethod() == null) {
				continue;
			}

			// Ignore unmanageable property.
			String attrName = pdesc.getName();
			Class<?> attrType = pdesc.getPropertyType();
			if (attrName.equals("class")) {
				continue;
			}
			if (!isReadable(type, attrName)) {
				continue;
			}

			// Ordinary property.
			String fqan = prefix + attrName;
			boolean writable = pdesc.getWriteMethod() != null;
			attributes.add(new ModelMBeanAttributeInfo(fqan, 
					convertType(object.getClass(), attrName, attrType, writable).getName(),
					pdesc.getShortDescription(), 
					true, 
					writable, 
					false));
			
			propertyDescriptors.put(fqan, pdesc);
		}
	}
	
	private void addOperations(List<ModelMBeanOperationInfo> operations, T object) {
		for (Method m : object.getClass().getMethods()) {
			String mname = m.getName();

			// Ignore getters and setters.
			if (mname.startsWith("is") || mname.startsWith("get") || mname.startsWith("set")) {
				continue;
			}

			// Ignore Object methods.
			if (mname.matches("(wait|notify|notifyAll|toString|equals|compareTo|hashCode|clone)")) {
				continue;
			}

			// Ignore other user-defined non-operations.
			if (!isOperation(mname, m.getParameterTypes())) {
				continue;
			}

			List<MBeanParameterInfo> signature = new ArrayList<MBeanParameterInfo>();
			int i = 1;
			for (Class<?> paramType : m.getParameterTypes()) {
				String paramName = "p" + (i++);
				if (getPropertyEditor(source.getClass(), paramName, paramType) == null) {
					continue;
				}
				signature.add(new MBeanParameterInfo(paramName, 
						convertType(null, null, paramType, true).getName(), 
						paramName));
			}

			Class<?> returnType = convertType(null, null, m.getReturnType(), false);
			operations.add(new ModelMBeanOperationInfo(m.getName(),
					m.getName(), 
					signature.toArray(new MBeanParameterInfo[signature.size()]),
					returnType.getName(), 
					ModelMBeanOperationInfo.ACTION));
		}

	}
	
	private boolean isWritable(Class<?> type, PropertyDescriptor pdesc) {
		if (type == null) {
			throw new NullPointerException("type");
		}
		if (pdesc == null) {
			return false;
		}
		String attrName = pdesc.getName();
		Class<?> attrType = pdesc.getPropertyType();
		boolean writable = pdesc.getWriteMethod() != null || isWritable(type, attrName);
		if (getPropertyEditor(type, attrName, attrType) == null) {
			writable = false;
		}
		return writable;
	}

	protected PropertyEditor getPropertyEditor(Class<?> type, String attrName, Class<?> attrType) {
		if (type == null) {
			throw new NullPointerException("type");
		}
		if (attrName == null) {
			throw new NullPointerException("attrName");
		}
		return null;
	}

	protected boolean isOperation(String methodName, Class<?>[] paramTypes) {
		return true;
	}

	private Class<?> convertType(Class<?> type, String attrName, Class<?> attrType, boolean writable) {
		if (attrName != null && (attrType == Long.class || attrType == long.class)) {
			if (attrName.endsWith("Time") && attrName.indexOf("Total") < 0
					&& attrName.indexOf("Min") < 0
					&& attrName.indexOf("Max") < 0
					&& attrName.indexOf("Avg") < 0
					&& attrName.indexOf("Average") < 0) {
				return Date.class;
			}
		}

		if (!writable) {
			if (Collection.class.isAssignableFrom(attrType) || Map.class.isAssignableFrom(attrType)) {
				if (List.class.isAssignableFrom(attrType)) {
					return List.class;
				}
				if (Set.class.isAssignableFrom(attrType)) {
					return Set.class;
				}
				if (Map.class.isAssignableFrom(attrType)) {
					return Map.class;
				}
				return Collection.class;
			}

			if (attrType.isPrimitive() || Date.class.isAssignableFrom(attrType)
					|| Boolean.class.isAssignableFrom(attrType)
					|| Character.class.isAssignableFrom(attrType)
					|| Number.class.isAssignableFrom(attrType)) {
				if (attrName == null || !attrName.endsWith("InMillis")) {
					return attrType;
				}
			}
		}

		return String.class;
	}
	
	public Object convertType(Class<?> attrType, Object value) {
		if (!(value instanceof String)) return value;
		
		String val = (String) value;
		if (attrType == Boolean.class || attrType == boolean.class) return Boolean.parseBoolean(val);
		if (attrType == Integer.class || attrType == int.class) return Integer.parseInt(val);
		if (attrType == Long.class || attrType == long.class) return Long.parseLong(val);
		if (attrType == Short.class || attrType == short.class) return Short.parseShort(val);
		if (attrType == Float.class || attrType == float.class) return Float.parseFloat(val);
		if (attrType == Double.class || attrType == double.class) return Double.parseDouble(val);
		
		return val;
	}
	
	protected boolean isReadable(Class<?> type, String attrName) {
		if (ThreadPoolExecutor.class.isAssignableFrom(type) && attrName.equals("queue")) {
			return false;
		}

		return true;
	}

	protected boolean isWritable(Class<?> type, String attrName) {
		return true;
	}
	
	private String getMethod(String name) {
		String first = name.substring(0, 1).toUpperCase();
		String left = name.substring(1);
		return "get" + first + left;
	}
	
	private String setMethod(String name) {
		String first = name.substring(0, 1).toUpperCase();
		String left = name.substring(1);
		return "set" + first + left;
	}
	
	public final T getSource() {
        return source;
    }
    
    public final MBeanServer getServer() {
        return server;
    }
    
    public final ObjectName getName() {
        return name;
    }

	public void setManagedResource(Object mr, String mr_type)
			throws MBeanException, RuntimeOperationsException,
			InstanceNotFoundException, InvalidTargetObjectTypeException {
		throw new RuntimeOperationsException(new UnsupportedOperationException());
	}

	public void setModelMBeanInfo(ModelMBeanInfo inModelMBeanInfo)
			throws MBeanException, RuntimeOperationsException {
		throw new RuntimeOperationsException(new UnsupportedOperationException());
	}

	public Object getAttribute(String attribute) throws AttributeNotFoundException, MBeanException,
			ReflectionException {
		Method[] methods = source.getClass().getMethods();
		String methodName = getMethod(attribute);
		for (Method method : methods) {
			if (method.getName().equals(methodName)) {
				try {
					return method.invoke(source, null);
				} catch (Exception e) {
					// Ignore.
				} 
			}
		}
		
		return null;
	}

	public AttributeList getAttributes(String[] attributes) {
		AttributeList answer = new AttributeList();
        for (int i = 0; i < attributes.length; i++) {
            try {
                answer.add(new Attribute(attributes[i], getAttribute(attributes[i])));
            } catch (Exception e) {
                // Ignore.
            }
        }
        return answer;
	}

	public MBeanInfo getMBeanInfo() {
		return info;
	}

	public Object invoke(String actionName, Object[] params, String[] signature)
			throws MBeanException, ReflectionException {
		
		// Handle synthetic operations first.
        if (actionName.equals("unregisterMBean")) {
            try {
                server.unregisterMBean(name);
                return null;
            } catch (InstanceNotFoundException e) {
            	// Ignore.
            }
        }
        
        //handle is operation
        int n = 0;
        Class<?>[] paramTypes = new Class<?>[params.length];
        for (Object param : params) {
        	paramTypes[n] = param.getClass();
        }
        
        if (!isOperation(actionName, paramTypes)) return null;
        
        //invoke method
        Method[] methods = source.getClass().getMethods();
		for (Method method : methods) {
			if (method.getName().equals(actionName)) {
				try {
					return method.invoke(source, params);
				} catch (Exception e) {
					// Ignore.
				} 
			}
		}
		
		return null;
	}

	public void setAttribute(Attribute attribute) throws AttributeNotFoundException, InvalidAttributeValueException,
			MBeanException, ReflectionException {
		String name = attribute.getName();
		Object value = attribute.getValue();
		Method[] methods = source.getClass().getMethods();
		String methodName = setMethod(name);
		for (Method method : methods) {
			if (method.getName().equals(methodName)) {
				try {
					PropertyDescriptor pdesc = propertyDescriptors.get(name);
					Class<?> attrType = pdesc.getPropertyType();
					Object param = convertType(attrType, value);
					method.invoke(source, param);
				} catch (Exception e) {
					// Ignore.
				} 
			}
		}
	}
	
	public AttributeList setAttributes(AttributeList attributes) {
		String names[] = new String[attributes.size()];
		int n = 0;
		
		for (Iterator<Attribute> it = (Iterator)attributes.iterator(); it.hasNext();) {
			Attribute attribute = it.next();
			try {
				names[n++] = attribute.getName();
				setAttribute(attribute);
			} catch (Exception e) {
				// Ignore.
			} 
		}
		return getAttributes(names);
	}

	public void load() throws MBeanException, RuntimeOperationsException, InstanceNotFoundException {
		throw new RuntimeOperationsException(new UnsupportedOperationException());
	}

	public void store() throws MBeanException, RuntimeOperationsException, InstanceNotFoundException {
		throw new RuntimeOperationsException(new UnsupportedOperationException());
	}

	public void addAttributeChangeNotificationListener(
			NotificationListener listener, String attributeName, Object handback)
			throws MBeanException, RuntimeOperationsException,
			IllegalArgumentException {
		
	}

	public void removeAttributeChangeNotificationListener(
			NotificationListener listener, String attributeName)
			throws MBeanException, RuntimeOperationsException,
			ListenerNotFoundException {
		
	}

	public void sendAttributeChangeNotification(
			AttributeChangeNotification notification) throws MBeanException,
			RuntimeOperationsException {
		
	}

	public void sendAttributeChangeNotification(Attribute oldValue,
			Attribute newValue) throws MBeanException,
			RuntimeOperationsException {
		
	}

	public void sendNotification(Notification ntfyObj) throws MBeanException,
			RuntimeOperationsException {
		
	}

	public void sendNotification(String ntfyText) throws MBeanException,
			RuntimeOperationsException {
		
	}

	public void addNotificationListener(NotificationListener listener,
			NotificationFilter filter, Object handback)
			throws IllegalArgumentException {
		
	}

	public MBeanNotificationInfo[] getNotificationInfo() {
		return new MBeanNotificationInfo[0];
	}

	public void removeNotificationListener(NotificationListener listener)
			throws ListenerNotFoundException {
		
	}

	public void postDeregister() {
		this.server = null;
        this.name = null;
	}

	public void postRegister(Boolean registrationDone) {
		
	}

	public void preDeregister() throws Exception {
		
	}

	public ObjectName preRegister(MBeanServer server, ObjectName name)
			throws Exception {
		this.server = server;
        this.name = name;
        return name;
	}

}
